home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Nebula 2
/
Nebula Two.iso
/
SourceCode
/
GameKit
/
Examples
/
PacMan
/
PacManView.m
< prev
next >
Wrap
Text File
|
1995-06-12
|
30KB
|
913 lines
#import "Text.h"
#import "PacManView.h"
#import "PacManGameBrain.h"
#import "PacManInfoController.h"
#import <libc.h> // event stuff, misc.
#import <daymisckit/daymisckit.h>
#import "FruitView.h"
#import "Maze.h"
#import "Monster.h"
#import "Player.h"
// comment out the next line to disable the cheat modes. Cheaters can't
// get net high scores, but they can get local high scores.
#define CHEATMODES YES
// sound definitions
#define DOTEATSOUND 0 // any normal dot
#define POWERDOTEATSOUND 2 // power dot
#define MONSTEREATSOUND 1 // monsters
#define FRUITEATSOUND 4 // fruit
#define DEADSOUND 3 // player nabbed by a ghost
#define STARTLEVELSOUND 5 // music for start of a game/level
#define POINTS (12.0 * scale) // the size (in points) we draw the text at
#define READY_X ((x - 2 * GHOST_SIZE) * scale + mazePos.x)
#define READY_Y (y * scale + mazePos.y)
#define OFFSET_X (scale * 6)
#define OFFSET_Y (scale * 3)
// decides which screen (1-6) to load for a given level. Allows us to
// put them in an arbitrary order.
static int screens[NUMSCREENS] = { // which maze image (1-6) to use
1, 1, 1, 1, 2, 2, 2, 2,
3, 3, 3, 4, 4, 5, 5, 5,
1, 2, 0, 0, 3, 4, 0, 5 };
static windowX[3] = { 0.0, 362.0, 698.0 };
static windowY[3] = { 0.0, 280.0, 536.0 };
@implementation PacManView
- initFrame:(const NXRect *)frm // designated initializer for a view
{
[super initFrame:frm];
backIsColor = NO; // override default; we want image by default
begin = 128;
// initialize game variables
// where the maze is located within our view.
mazePos.x = 13; mazePos.y = 12;
eraseReady = NO;
myorigin.x = 0; myorigin.y = 0;
scale = 0;
// a convenient rect; this way I don't have to keep creating a bunch of
// rects of dimensions GHOST_SIZE by GHOST_SIZE; I re-use this one...
NXSetRect(&eraseRect, 0, 0, GHOST_SIZE, GHOST_SIZE);
NXSetRect(&textEraseRect, 0, 0, 3 * GHOST_SIZE, 2 * GHOST_SIZE);
NXSetRect(&readyRect, 0, 0, GHOST_SIZE * scale * 5, GHOST_SIZE * scale);
// make sure power dots get drawn
erasePwr = YES;
return self;
}
- setGhostTracker:(GKTrackerId)tracker { ghostId = tracker; return self; }
- setFruitTracker:(GKTrackerId)tracker { fruitId = tracker; return self; }
// Called by appDidInit to load up images, etc. that we need.
- loadPix
{
int i; id dotSound = [customSound soundNum:DOTEATSOUND type:0];
const int *gh;
[super loadPix];
// the dots play through teir own private stream with our special params
[dotSound setPlayStream:[[GKSoundStream alloc] initStreams:1]];
[dotSound setPercentToPlay:0.01]; // this number works well... :-)
backGround2 = [[NXImage allocFromZone:[self zone]] initSize:&bounds.size];
fruitPointCount = 0;
// build the ghost objects
gh = [maze ghosts];
for (i=0; i<=3; i++) {
ghost[i] = [[Monster alloc]
initGhost:i player:player maze:maze at:gh[i*2] :gh[i*2+1]];
ghostPointCount[i] = 0;
}
// get the images
fruit[1] = [NXImage findImageNamed:"Fruit.tiff"];
fruit[2] = [NXImage findImageNamed:"FruitBig.tiff"];
gameOver[1] = [NXImage findImageNamed:"GameOver.tiff"];
gameOver[2] = [NXImage findImageNamed:"GameOverBig.tiff"];
[gameOver[1] getSize:&(gameOverSize[1])];
[gameOver[2] getSize:&(gameOverSize[2])];
[self setScale:[preferences scale]];
return self;
}
- ghost:(int)i // return ghost #i
{
return ghost[i];
}
// State machine...called by the timed entry. This state machine basically
// controls all the aspects of the game; depending upon it's state, different
// things can/will happen. The best way to figure this one out is to sit
// down and draw a diagram show how the states transition from one to the
// next. Note that there are several counters, etc. that function as smaller
// independent state machines that operate within the context of this larger
// state machine. (Dots blinking, monster states, etc. all run independently
// from main state machine, although the main machine occasionally interrupts
// things in the sub-machines.) I've not yet had time to fully document the
// logic involved in this state machine...if I ever have the time, I may do so,
// but I don't know that anyone would care if I did anyway. Minor changes in
// here could render the entire game non-functional if you aren't careful!
// UNLESS YOU KNOW WHAT YOU'RE DOING, DON'T MESS WITH THIS CODE! Take time
// to understand it fully before playing with it!
// This is the most ridiculously long method you'll ever see, but there's
// no really efficient way to break it up. (Each section deals with what
// happens in a specific state, and breaking into smaller methods isn't worth
// while, since things are repeated...and it's silly to proliferate subroutines
// that only get called once and from one place.)
- autoUpdate:sender
{ // ALL animation is controlled from here!!!
register int i, lcnt;
register BOOL dotEat = NO;
int x, y;
BOOL flag = NO;
BOOL updateFlag = NO;
float gx, gy;
NXRect tRect;
// keep track of how many time we've been called; we can use it do
// decide when to do certain things...
if ([NXApp isHidden]) return self; // don't suck cycles if we're hidden
cycles++;
if (((![preferences speed]) || ([preferences speed] == 2))
&& (!demoMode)) { // slow speed, so ignore 50% of entries
if (cycles & 0x01) return self;
}
if (!(cycles & 0x0f)) { // power dots always blink no matter what.
[self blinkPowerDot]; // happens every 16 cycles.
erasePwr = YES;
}
if (fruitPointCount) {
if (!--fruitPointCount) {
fruitPointCount = WIPETEXT;
} }
for (i=0; i<4; i++) {
if (ghostPointCount[i]) {
if (!--ghostPointCount[i]) {
ghostPointCount[i] = WIPETEXT;
} } }
if (state == GAMEOVER) {
// when demowait counter hits WAITFORDEMO, we start up demo mode.
// it basically restarts the game--but with "demoMode" turned on.
if (demoWait++ == WAITFORDEMO) {
state = NORMALSTATE;
[self restartGameForDemo:YES];
[controller unpause];
[[self window] setTitle:"PacMan Demo"];
[scoreKeeper resetScore];
} }
// in this state, the "Get Ready!" sign has been put up; we stall for a]
// while and then we take it away and start up the next level.
if ((state == READY) || (state == DIEREADY)) {
for (i=0; i<=3; i++) {
[ghost[i] move:self];
}
if (!(--begin)) {
eraseReady = YES;
if (demoMode) [player resetPlayer];
state = NORMALSTATE;
updateFlag = YES;
} }
// make the maze blink on and off at the end of the level.
if (state == BLINK_LEVEL) {
if (begin--) {
if (begin < 32) {
if (!(cycles & 0x03)) { // make the maze blink
[maze visible:(![maze isVisible])];
updateFlag = YES;
} }
} else {
state = READY;
[maze visible:YES];
[player resetPlayer];
[controller nextLevel];
updateFlag = YES;
begin = 40;
}
}
// stall. when player dies, the player object needs time to get through
// it's whole sequence.
if (state == DYING_PAC) {
if (!(--begin)) { // stall
if (![player newPlayer]) { // no pacs left == game over.
[controller gameOver];
state = GAMEOVER;
updateFlag = YES;
} else { // do ready, next pac...
state = DIEREADY;
[self startScreen];
[player resetPlayer];
updateFlag = YES;
begin = 40;
} } }
// this state is the meat of the game. move the player and monsters
// and deal with player/ghost collisions and eating dots and fruit.
if (state == NORMALSTATE) {
// move all the ghosts and the pac
if (!paused) {
if (demoMode) lcnt = 2;
else {
lcnt = [preferences speed] / 2 + 1; // allow hyper speeds:
// since 0 <= speed <= 3, we'll move one or two frames per update
}
while (lcnt) {
// figure out how to move player
[player move:self];
// put up/take away the fruit
[maze playerPosition:&x :&y]; // get position of fruit
if ((numFruits < 3)||demoMode) { // only two fruits per level
if (fruitCount++ == timeToFruit) { // time for new fruit ?
numFruits++;
if (numFruits < 3) fruitOn = DRAW;
NXSetRect(&tRect, x * scale + mazePos.x,
y * scale + mazePos.y, FRUIT_SIZE * scale,
FRUIT_SIZE * scale);
[self rebuildStaticAt:&tRect];
} else // one or the other
if (fruitCount == ERASEFRUIT) {
// it's been there a while... so remove it
timeToFruit = 200 + (random() & 0x01f0);
fruitOn = ERASE;
if (numFruits == 2) numFruits++;
NXSetRect(&tRect, x * scale + mazePos.x,
y * scale + mazePos.y, FRUIT_SIZE * scale,
FRUIT_SIZE * scale);
[self rebuildStaticAt:&tRect];
fruitCount = 0;
}
}
// handle eating dots
if ([maze eatDotAt:[player xpos] :[player ypos]]) {
[customSound playNum:DOTEATSOUND];
dotEat = YES;
}
if ([player pacAlive]) { // if double timing, need to check.
if ([maze powerDotAt:[player xpos] :[player ypos]]) {
[customSound playNum:POWERDOTEATSOUND];
[scoreKeeper resetBonus:ghostId];
flag = YES;
dotEat = YES;
} }
if (dotEat) {
[maze lastDot:&x :&y];
NX_X(&eraseRect) = x * scale + mazePos.x;
NX_Y(&eraseRect) = y * scale + mazePos.y;
[self rebuildStaticAt:&eraseRect];
dotEat = NO;
}
if (![maze dots]) {
state = BLINK_LEVEL;
begin = 64;
fruitOn = ERASE; // erase fruit if finished level
fruitPointCount = 0;
for (i=0; i<4; i++) ghostPointCount[i] = 0;
[maze playerPosition:&x :&y]; // get position of fruit
NXSetRect(&tRect, x * scale + mazePos.x,
y * scale + mazePos.y, FRUIT_SIZE * scale,
FRUIT_SIZE * scale);
[self rebuildStaticAt:&tRect];
fruitCount = 0;
}
// see if player ate the fruit
[maze playerPosition:&x :&y]; // get position of fruit
if (fruitOn == YES) { // fruit's there...
if ((abs([player ypos] - y) < GHOST_SIZE / 2) &&
(abs([player xpos] - x) < GHOST_SIZE / 2)) { // ate it!
fruitOn = ERASE;
[customSound playNum:FRUITEATSOUND];
NXSetRect(&tRect, x * scale + mazePos.x,
y * scale + mazePos.y, FRUIT_SIZE * scale,
FRUIT_SIZE * scale);
[self rebuildStaticAt:&tRect];
// add value of fruit to the score
fruitPoints = [scoreKeeper
addBonusToScore:fruitId advance:NO];
// tell the player how many points he/she got
ftx = (x + TEXTOFFSET) * scale + mazePos.x;
fty = y * scale + mazePos.y;
ftx2 = ftx + OFFSET_X; fty2 = fty + OFFSET_Y;
// now, align coords to the maze so erase
// does it's job right (otherwise, we end up SOVERing
// maze parts twice, and it looks _ugly_!
ftx -= mazePos.x; fty -= mazePos.y;
// chop off lower four bits (floor to mult. of 16)
ftx /= 16 * scale;
ftx = (ftx << (scale + 3)) + mazePos.x;
fty /= 16 * scale;
fty = (fty << (scale + 3)) + mazePos.y;
ZAPRECT(textEraseRect, ftx, fty);
fruitPointCount = 48;
} }
// check for player/ghost collision: (and deal with it)
if ([maze dots]) {
for (i=0; i<=3; i++) {
// tell ghost of power dot
if (flag) [ghost[i] powerDot:YES];
[ghost[i] at:&gx :&gy];
if ((abs(gx - [player xpos]) < GHOST_SIZE / 2) &&
(abs(gy - [player ypos]) < GHOST_SIZE / 2)) {
// collision! decide who dies...
switch ([ghost[i] munch]) {
case YES : { // player got ghost
[customSound playNum:MONSTEREATSOUND];
// (-munch already added any bonus.)
gtx[i] = (gx + TEXTOFFSET) * scale
+ mazePos.x;
gty[i] = gy * scale + mazePos.y;
gtx2[i] = gtx[i] + OFFSET_X;
gty2[i] = gty[i] + OFFSET_Y;
ghostPoints[i] = [scoreKeeper
addBonusToScore:ghostId
advance:YES];
// now, align to maze as above
gtx[i] -= mazePos.x; gtx[i] /= scale * 16;
gty[i] -= mazePos.y; gty[i] /= scale * 16;
gtx[i] = (gtx[i] << (3 + scale))
+ mazePos.x;
gty[i] = (gty[i] << (3 + scale))
+ mazePos.y;
ZAPRECT(textEraseRect, gtx[i], gty[i]);
ghostPointCount[i] = 48;
break;
}
case NO : { // ghost got player, so die...
if (![player pacAlive]) break;
[customSound playNum:DEADSOUND];
if (cheatMode) break;
state = DYING_PAC;
begin = 48; // stall
[player pacDie];
// now, erase the fruit
fruitOn = ERASE;
[maze playerPosition:&x :&y];
NXSetRect(&tRect, x * scale + mazePos.x,
y * scale + mazePos.y,
FRUIT_SIZE * scale,
FRUIT_SIZE * scale);
[self rebuildStaticAt:&tRect];
updateFlag = YES;
break;
}
case HARMLESS :
default : { // eyes do nothing.
break;
} } } } }
// figure out where ghosts will go next
for (i=0; i<=3; i++) {
[ghost[i] move:self];
}
if (lcnt > 1) { // make movement take effect w/o render
// doing this extra movement gives faster perceived speeds
for (i=0; i<=3; i++) {
[ghost[i] moveOneFrame];
[ghost[i] powerCount];
}
[player moveOneFrame];
}
lcnt--;
} } }
// draw all the changes, if applicable.
if (updateFlag) [self update];
/*if ((state != READY) && (state != DIEREADY))*/ [self updateSelf:&bounds :1];
return self;
}
// This renders the whole screen, much like updateSelf:: below, but since
// it always redraws the _entire_ screen, it is unnaceptably inefficient for
// handling individual animation frames.
- drawSelf:(NXRect *)rects :(int)rectCount // redraws the screen.
{ // right now, it's stupid and always redraws the whole view.
//register int i;//, f;
//int x, y;
NXPoint pos;
//NXRect from;//, bezel, mazeRect;
if ([self window] == nil)
return self; // exit if no window to draw in
if ((state == BLINK_LEVEL) && ![maze isVisible])
[dirtPile fullRedraw:self :backGround2];
else [dirtPile fullRedraw:self :staticBuffer];
if (state == GAMEOVER) {
pos.x = (NX_WIDTH(&bounds) - gameOverSize[scale].width) / 2;
pos.y = (NX_HEIGHT(&bounds) - gameOverSize[scale].height) / 2;
[gameOver[scale] composite:NX_SOVER toPoint:&pos];
}
if (NXDrawingStatus == NX_PRINTING) { // make sure actors are in print
[self updateSelf:rects :rectCount];
}
NXPing();
return self;
}
// This is the main drawing here. It works like drawSelf, but only
// _changes_ stuff; it doesn't re-draw the whole view each time. This
// has been done to speed things up. We erase the old, and then redraw
// all the spots we erased after calculating where things have moved to.
- updateSelf:(NXRect *)rects :(int)rectCount // redraws the screen.
{ // it redraws only what has changed since last redraw.
register int i;
int x, y, j, order[4], flag[4];
// NXRect from;
if ([self window] == nil) return self; // exit if no window to draw in
// if blinking maze and maze isn't on screen then this update is unneeded
// note that technically, even if maze is visible, we could skip out, but
// we don't because we want the last dot and the monsters, etc. to go
// away right as we enter BLINK_LEVEL; this won't happen otherwise.
if (((state == BLINK_LEVEL) && ![maze isVisible]) || (state == GAMEOVER))
return self;
[buffer lockFocus];
// erase ghosts and ghost text fields.
for (i=0; i<=3; i++) {
if ((ghostPointCount[i] == WIPETEXT) && (state != GAME_OVER)) {
ghostPointCount[i] = 0;
ZAPRECT(textEraseRect, gtx[i], gty[i]);
CLRRECT(textEraseRect);
}
if (state != GAME_OVER) {
[ghost[i] lastAt:&NX_X(&eraseRect) :&NX_Y(&eraseRect)];
NX_X(&eraseRect) = NX_X(&eraseRect) * scale + mazePos.x;
NX_Y(&eraseRect) = NX_Y(&eraseRect) * scale + mazePos.y;
CLRRECT(eraseRect);
}
}
// erase ready text
if (eraseReady) {
[maze playerPosition:&x :&y];
eraseReady = NO;
ZAPRECT(readyRect, READY_X, READY_Y); // erase ready text
CLRRECT(readyRect);
}
// erase fruit text
if ((fruitPointCount == WIPETEXT) && (state != GAME_OVER)) {
fruitPointCount = 0;
ZAPRECT(textEraseRect, ftx, fty);
CLRRECT(textEraseRect);
}
// erase the player's PacMan
if (state != GAME_OVER) {
[player lastAt:&NX_X(&eraseRect) :&NX_Y(&eraseRect)];
NX_X(&eraseRect) = NX_X(&eraseRect) * scale + mazePos.x;
NX_Y(&eraseRect) = NX_Y(&eraseRect) * scale + mazePos.y;
CLRRECT(eraseRect);
}
// put the player's Pac on the screen if in a state where it's visible.
if ((state != BLINK_LEVEL) || ((state == BLINK_LEVEL) && (begin > 31))) {
[player renderAt:mazePos.x :mazePos.y
move:((!paused) && (state == NORMALSTATE))];
[player lastAt:&NX_X(&eraseRect) :&NX_Y(&eraseRect)];
NX_X(&eraseRect) = NX_X(&eraseRect) * scale + mazePos.x;
NX_Y(&eraseRect) = NX_Y(&eraseRect) * scale + mazePos.y;
[dirtPile addRegion:&eraseRect];
}
// put ghosts on screen if we're in a state where they are visible.
if ((state != DYING_PAC) && (state != BLINK_LEVEL) &&
(state != GAMEOVER)) {
for (i=0; i<=3; i++) {
[ghost[i] renderAt:mazePos.x :mazePos.y move:
((!paused) && (state == NORMALSTATE))];
[ghost[i] lastAt:&NX_X(&eraseRect) :&NX_Y(&eraseRect)];
NX_X(&eraseRect) = NX_X(&eraseRect) * scale + mazePos.x;
NX_Y(&eraseRect) = NX_Y(&eraseRect) * scale + mazePos.y;
[dirtPile addRegion:&eraseRect];
} }
// draw text messages
if (fruitPointCount) {
drawScore(ftx2, fty2, POINTS, fruitPoints);
ZAPRECT(textEraseRect, ftx, fty);
}
for (i=0; i<4; i++) flag[i] = NO;
for (i=0; i<4; i++) { // sort scores to draw in lowest to highest order
// A selection sort is used. O(n^2) but n=4 so who cares?
int next = 0; int val = 100000;
for (j=0; j<4; j++) { // find next lowest, unused score value
if (!flag[j] && (ghostPoints[j] < val)) {
val = ghostPoints[j]; next = j;
}
}
flag[next] = YES; order[i] = next;
}
for (i=0; i<4; i++) if (ghostPointCount[order[i]]) {
drawScore(gtx2[order[i]], gty2[order[i]], POINTS,
ghostPoints[order[i]]);
ZAPRECT(textEraseRect, gtx[order[i]], gty[order[i]]);
}
if ((state == READY) || (state == DIEREADY)) {
[maze playerPosition:&x :&y];
drawReady(READY_X + 10 * scale, READY_Y + OFFSET_Y, POINTS);
ZAPRECT(readyRect, READY_X, READY_Y);
}
[buffer unlockFocus];
// housekeeping for the graphics:
[self lockFocus];
[dirtPile doRedraw:buffer]; // flush buffer out
[self unlockFocus];
if (fruitOn == ERASE) fruitOn = NO;
NXPing();
erasePwr = NO; // we've listened to this flag, so turn it off now.
return self;
}
// Handle player movement. We respond to the arrow keys. If we don't
// recognize the key, we pass it on. This method just shunts the key
// off to the player object, which is what really deals with it.
- keyDown:(NXEvent *)myevent
{
unsigned short charCode;
unsigned short charSet;
register int i;
if (!myevent) return self; // if no event when coalescing, go away
PSobscurecursor();
#ifdef CHEATMODES
// secret cheat mode: control-c
if (myevent->data.key.charCode == 0x03) {
cheatMode = YES;
fflush(stderr);
[preferences setUnfair]; // don't allow net high scores. local OK
[customSound playNum:POWERDOTEATSOUND];
return self;
}
// secret cheat: control-d (eats a power dot)
if (myevent->data.key.charCode == 0x04) {
for (i=0; i<4; i++) [ghost[i] powerDot:YES];
[preferences setUnfair]; // don't allow net high scores. local OK
[customSound playNum:POWERDOTEATSOUND];
[scoreKeeper resetBonus:ghostId];
return self;
}
// secret cheat: control-f (put fruit on screen)
if (myevent->data.key.charCode == 0x06) {
fruitCount = timeToFruit - 1;
numFruits = 0;
[preferences setUnfair]; // don't allow net high scores. local OK
return self;
}
// secret cheat: control-l (advance a level instantly)
if (myevent->data.key.charCode == 0x0c) {
NXRect tRect;
int x, y;
state = BLINK_LEVEL;
begin = 64;
fruitPointCount = 0; for (i=0; i<4; i++) ghostPointCount[i] = 0;
fruitOn = ERASE; // erase fruit if finished level
[maze playerPosition:&x :&y]; // get position of fruit
NXSetRect(&tRect, x * scale + mazePos.x,
y * scale + mazePos.y, FRUIT_SIZE * scale,
FRUIT_SIZE * scale);
[self rebuildStaticAt:&tRect];
fruitCount = 0;
[preferences setUnfair]; // don't allow net high scores. local OK
return self;
}
#endif
// check for arrow keys or space bar
if (!(myevent->flags&(NX_CONTROLMASK|NX_ALTERNATEMASK|NX_COMMANDMASK))) {
charCode = myevent->data.key.charCode;
charSet = myevent->data.key.charSet;
if (charSet == NX_SYMBOLSET) { // symbol set contains the arrows
if (charCode == 0xAD) { // Up Arrow
[player newDirection:PAC_UP];
if (paused) [controller unpause];
return self;
} else if (charCode == 0xAF) { // Down Arrow
[player newDirection:PAC_DOWN];
if (paused) [controller unpause];
return self;
} else if (charCode == 0xAC) { // Left Arrow
[player newDirection:PAC_LEFT];
if (paused) [controller unpause];
return self;
} else if (charCode == 0xAE) { // Right Arrow
[player newDirection:PAC_RIGHT];
if (paused) [controller unpause];
return self;
}
} else if (myevent->data.key.charCode == ' ') { // Space Bar
[player newDirection:PAC_STOP];
if (paused) [controller unpause];
return self;
} else if ((myevent->data.key.charCode == 'a') && cheatMode) {
#ifdef CHEATMODES
// abort pac -- it's the only way you can die in cheat mode.
[customSound playNum:DEADSOUND];
state = DYING_PAC;
begin = 48; // stall
[player pacDie];
#endif
} else {
[super keyDown:myevent];
}
} else [super keyDown:myevent];
[self keyDown:[NXApp peekAndGetNextEvent:NX_KEYDOWN]]; // coalesce keydowns
// this is done recursively...primitive, but easy to implement :-)
return self;
}
// set up the screen at the start of a new level. Loads in the maze.
- setUpScreen
{
[maze makeMaze:screens[(([controller level] >= NUMSCREENS) ?
(NUMSCREENS - 1) : [controller level]) - 1]];
fruitOn = NO;
[self rebuildStaticBuffer];
[self startScreen];
timeToFruit = 200 + (random() & 0x01f0);
fruitCount = 0;
numFruits = 0;
[[scoreKeeper bonusTracker:fruitId] advanceBonus]; // changes with level
[super setUpScreen];
return self;
}
// This lets us put the ghosts back where they are at the start of the level.
// This is separate from above because the above also resets the dots, and if
// the player dies, we only want to reset the ghosts, not the dots, too.
- startScreen
{
const int *gh; int i;
gh = [maze ghosts]; // pointer to array of ghost coordinates
for (i=0; i<=3; i++) { // re-initialize each ghost
[ghost[i] initGhost:i player:player maze:maze at:gh[i*2] :gh[i*2+1]];
[ghost[i] setScale:scale];
ghostPointCount[i] = 0;
}
fruitPointCount = 0;
// don't let fruit come out too quickly
if (timeToFruit - fruitCount < 200)
timeToFruit += 250;
return self;
}
- restartGame
{
return [self restartGameForDemo:NO];
}
- restartGameForDemo:(BOOL)doingDemo
{
// go to READY state to start game; but want DIEREADY so we don't advance
// the level; the controller, by virtue of calling this method, has already
// advanced the level.
state = DIEREADY;
begin = 100;
fruitOn = NO;
// make sure that all artifacts of demo mode are gone
demoWait = 0;
cheatMode = NO;
if (demoMode) {
demoMode = NO;
[[self window] setTitle:"PacMan"];
}
[scoreKeeper resetScore]; // clear the score
[scoreKeeper resetBonus:(-1)]; // clear all bonuses
[self getPreferences]; // make sure we're up to date
[player newPlayer]; // get a new pac to play with
// (above always sets up the pac, but in demoMode, the gameBrain hasn't
// given us 3 pacs, so we end up taking a negative # of pacs, which means
// demo mode will end as soon as the pac dies.)
if (doingDemo) demoMode = YES;
// cut off the info panel sound (if playing) (only if not demo mode)
else [[controller infoController] stopSound:self];
[customSound shutUpUntil:[[DAYTime alloc] initWithCurrentTime]];
[self update]; [self updateSelf:&bounds :1]; NXPing(); // show screen
// start up the sound player object with the "start game" music
if ([preferences music]) [customSound playNum:STARTLEVELSOUND];
return self;
}
- setBackgroundFile:(const char *)fileName andRemember:(BOOL)remember
{
NXRect bezel = {{NX_X(&bounds) + 5, NX_Y(&bounds) + 5},
{NX_WIDTH(&bounds) - 10, NX_HEIGHT(&bounds) - 10}};
[super setBackgroundFile:fileName andRemember:remember];
[backGround2 lockFocus];
NXDrawGrayBezel(&bezel, &bounds);
NXFrameRectWithWidth(&bounds, 5);
NX_WIDTH(&bezel) -= 6; NX_HEIGHT(&bezel) -= 6;
NX_X(&bezel) += 3; NX_Y(&bezel) += 3;
[self drawBackground:&bezel];
[backGround2 unlockFocus];
[self rebuildStaticBuffer];
[self update];
return self;
}
- buildColorBackground
{
NXRect bezel = {{NX_X(&bounds) + 5, NX_Y(&bounds) + 5},
{NX_WIDTH(&bounds) - 10, NX_HEIGHT(&bounds) - 10}};
[backGround2 lockFocus];
NXDrawGrayBezel(&bezel, &bounds);
NXFrameRectWithWidth(&bounds, 5);
NX_WIDTH(&bezel) -= 6; NX_HEIGHT(&bezel) -= 6;
NX_X(&bezel) += 3; NX_Y(&bezel) += 3;
[self drawBackground:&bezel];
[backGround2 unlockFocus];
[self rebuildStaticBuffer];
return self;
}
- acceptColor:(NXColor)color atPoint:(const NXPoint *)aPoint
{ // override to redraw background buffer.
backIsColor = YES;
backColor = color;
[self buildColorBackground];
[[self writeColor] update];
return self;
}
- rebuildStaticBuffer
{
NXRect mazeRect = {{0, 0},
{GHOST_SIZE * BLOCK_WIDTH, GHOST_SIZE * BLOCK_HEIGHT}};
int x, y;
register int f;
NXRect from;
NXPoint pos;
[staticBuffer lockFocus];
[backGround2 composite:NX_COPY fromRect:&bounds toPoint:&(bounds.origin)];
[maze render:&mazeRect at:&mazePos];
[maze playerPosition:&x :&y];
if (fruitOn) {
if (fruitOn != ERASE) {
// decide which fruit to draw
f = fruits[(([controller level] > FRUIT_LEVELS) ?
FRUIT_LEVELS : [controller level])];
// draw the fruit
pos.x = x * scale + mazePos.x;
pos.y = y * scale + mazePos.y;
NXSetRect(&from,
(f % FRUIT_PER_ROW) * FRUIT_SIZE * scale,
(f / FRUIT_PER_ROW) * FRUIT_SIZE * scale,
FRUIT_SIZE * scale, FRUIT_SIZE * scale);
[fruit[scale] composite:NX_SOVER fromRect:&from toPoint:&pos];
fruitOn = YES;
} else fruitOn = NO;
}
[staticBuffer unlockFocus];
[buffer lockFocus];
[staticBuffer composite:NX_COPY fromRect:&bounds toPoint:&(bounds.origin)];
[buffer unlockFocus];
[dirtPile addRegion:&bounds];
return self;
}
- rebuildStaticAt:(NXRect *)rect;
{ // assumes that bezel is intact, so doesn't draw it
NXRect mazeRect = {{(NX_X(rect) - mazePos.x) / scale,
(NX_Y(rect) - mazePos.y) / scale},
{(NX_WIDTH(rect) - 1.0) / scale,
(NX_HEIGHT(rect) - 1.0) / scale}};
NXRect xRect = {{0.0, 0.0}, {GHOST_SIZE * scale, GHOST_SIZE * scale}};
NXPoint pos;
NXRect from;
register int f;
int x, y;
[staticBuffer lockFocus];
[super rebuildStaticAt:rect];
[maze render:&mazeRect at:&mazePos];
// see if rect intersects fruit && fruit is on screen
[maze playerPosition:&x :&y];
NX_X(&xRect) = x * scale + mazePos.x;
NX_Y(&xRect) = y * scale + mazePos.y;
if (NXIntersectsRect(&xRect, rect) && fruitOn) {
if (fruitOn != ERASE) {
// decide which fruit to draw
f = fruits[(([controller level] > FRUIT_LEVELS) ?
FRUIT_LEVELS : [controller level])];
// draw the fruit
pos.x = x * scale + mazePos.x;
pos.y = y * scale + mazePos.y;
NXSetRect(&from,
(f % FRUIT_PER_ROW) * FRUIT_SIZE * scale,
(f / FRUIT_PER_ROW) * FRUIT_SIZE * scale,
FRUIT_SIZE * scale, FRUIT_SIZE * scale);
[fruit[scale] composite:NX_SOVER fromRect:&from
toPoint:&(xRect.origin)];
fruitOn = YES;
} else fruitOn = NO;
}
[staticBuffer unlockFocus];
[buffer lockFocus];
[staticBuffer composite:NX_COPY fromRect:rect toPoint:&(rect->origin)];
[buffer unlockFocus];
[dirtPile addRegion:rect];
return self;
}
- blinkPowerDot // update static buffer where power dots are.
{
const int *pd; // pointer to array of power dot coords
register int i;
[maze blinkPowerDot];
pd = [maze powerDot]; // get power dot coords
for (i=0; i<=3; i++) { // erase power dot every time it blinks.
NX_X(&eraseRect) = pd[i * 2 ] * GHOST_SIZE * scale + mazePos.x;
NX_Y(&eraseRect) = pd[i * 2 + 1] * GHOST_SIZE * scale + mazePos.y;
[self rebuildStaticAt:&eraseRect];
}
return self;
}
- (int)scale
{
return scale;
}
- setScale:(int)newScale
{
int i;
if ((newScale < 1) || (newScale > 2)) return self;
if (newScale == scale) return self; // already that size.
scale = newScale;
for (i=0; i<4; i++) {
[ghost[i] setScale:scale];
}
[player setScale:scale];
[maze setScale:scale];
// resize self and windows
[window disableFlushWindow];
[window disableDisplay];
[self sizeTo:windowX[scale] :windowY[scale]];
NXSetRect(&eraseRect, 0, 0, GHOST_SIZE * scale, GHOST_SIZE * scale);
NXSetRect(&readyRect, 0, 0, GHOST_SIZE * scale * 5, GHOST_SIZE * scale);
NXSetRect(&textEraseRect, 0, 0,
3 * GHOST_SIZE * scale, 2 * GHOST_SIZE * scale);
// rebuild all the offscreen buffers.
[buffer free];
[staticBuffer free];
[backGround2 free];
buffer = [[NXImage allocFromZone:[self zone]] initSize:&bounds.size];
staticBuffer = [[NXImage allocFromZone:[self zone]] initSize:&bounds.size];
backGround2 = [[NXImage allocFromZone:[self zone]] initSize:&bounds.size];
if (backIsColor) [self buildColorBackground];
else [self setBackgroundFile:NXGetDefaultValue([NXApp appName],
"BackGround") andRemember:NO];
[self rebuildStaticBuffer];
[[window delegate] windowDidMove:window];
[window sizeWindow:windowX[scale] :windowY[scale]];
[window reenableFlushWindow];
[window reenableDisplay];
[[window display] flushWindowIfNeeded];
[self update];
return self;
}
@end